Fire cases in London 2019-2022

Group number 21

Names of students:

Shahar Lavi
Yeheli Rot

From: Link to Kaggle: https://www.kaggle.com/datasets/timmofeyy/-fire-cases-in-uk-within-last-3-years/data
Kaggle sources: https://data.london.gov.uk/dataset/london-fire-brigade-incident-records.


No description has been provided for this image

Table of Contents:

  1. Getting to know+clearing the data
  2. First path
  3. Second path
  4. Third path
  5. Summary
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
import numpy as np
import seaborn as sns
from IPython.display import display, HTML
import requests
import json

Getting to know the data:

  • הבנת העמודות והנתונים
  • סינון נתונים לא רלוונטים ובדיקת עמודות ריקות
  • הצגת הקורולציה

url1 = 'https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/fire_cases_part_1.csv'
url2 = 'https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/fire_cases_part_2.csv'
url3='https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/fire_cases_part_3.csv'
url4='https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/fire_cases_part_4.csv'
url5='https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/fire_cases_part_5.csv'
url6='https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/fire_cases_part_6.csv'
fire_df1 = pd.read_csv(url1)
fire_df2 = pd.read_csv(url2)
fire_df3 = pd.read_csv(url3)
fire_df4 = pd.read_csv(url4)
fire_df5 = pd.read_csv(url5)
fire_df6 = pd.read_csv(url6)
fire_df = pd.concat([fire_df1 ,fire_df2,fire_df3,fire_df4,fire_df5,fire_df6]).reset_index()
fire_df
index IncidentNumber DateOfCall CalYear TimeOfCall HourOfCall IncidentGroup StopCodeDescription SpecialServiceType PropertyCategory ... FirstPumpArriving_AttendanceTime FirstPumpArriving_DeployedFromStation SecondPumpArriving_AttendanceTime SecondPumpArriving_DeployedFromStation NumStationsWithPumpsAttending NumPumpsAttending PumpCount PumpHoursRoundUp Notional Cost (£) NumCalls
0 0 000006-01012019 01 Jan 2019 2019 00:01:45 0 Special Service Special Service Lift Release Dwelling ... NaN NaN NaN NaN 1.0 1.0 1.0 1.0 333.0 2.0
1 1 000019-01012019 01 Jan 2019 2019 00:04:33 0 Fire Secondary Fire NaN Outdoor ... 357.0 Edmonton NaN NaN 1.0 1.0 1.0 1.0 333.0 1.0
2 2 000020-01012019 01 Jan 2019 2019 00:04:39 0 False Alarm False alarm - Good intent NaN Outdoor ... 318.0 Southgate NaN NaN 1.0 1.0 1.0 1.0 333.0 1.0
3 3 000021-01012019 01 Jan 2019 2019 00:04:44 0 False Alarm AFA NaN Dwelling ... 210.0 Kensington NaN NaN 1.0 1.0 1.0 1.0 333.0 1.0
4 4 000024-01012019 01 Jan 2019 2019 00:05:00 0 Special Service Special Service Lift Release Dwelling ... 329.0 Bethnal Green NaN NaN 1.0 1.0 1.0 1.0 333.0 1.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
331565 55260 030044-28022022 28 Feb 2022 2022 23:31:12 23 Special Service Special Service Effecting entry/exit Dwelling ... 260.0 Heston NaN NaN 1.0 1.0 1.0 1.0 352.0 1.0
331566 55261 030045-28022022 28 Feb 2022 2022 23:40:48 23 Fire Primary Fire NaN Outdoor Structure ... 349.0 Hornchurch NaN NaN 2.0 2.0 2.0 3.0 1056.0 1.0
331567 55262 030046-28022022 28 Feb 2022 2022 23:43:54 23 False Alarm AFA NaN Non Residential ... 339.0 Battersea NaN NaN 1.0 1.0 1.0 1.0 352.0 1.0
331568 55263 030047-28022022 28 Feb 2022 2022 23:43:58 23 False Alarm AFA NaN Other Residential ... 178.0 Dowgate NaN NaN 1.0 1.0 1.0 1.0 352.0 1.0
331569 55264 030048-28022022 28 Feb 2022 2022 23:44:02 23 Special Service Special Service Effecting entry/exit Dwelling ... 390.0 Tottenham NaN NaN 1.0 1.0 1.0 1.0 352.0 1.0

331570 rows × 40 columns

fire_df.shape
(331570, 40)
fire_df.columns
Index(['index', 'IncidentNumber', 'DateOfCall', 'CalYear', 'TimeOfCall',
       'HourOfCall', 'IncidentGroup', 'StopCodeDescription',
       'SpecialServiceType', 'PropertyCategory', 'PropertyType',
       'AddressQualifier', 'Postcode_full', 'Postcode_district', 'UPRN',
       'USRN', 'IncGeo_BoroughCode', 'IncGeo_BoroughName', 'ProperCase',
       'IncGeo_WardCode', 'IncGeo_WardName', 'IncGeo_WardNameNew', 'Easting_m',
       'Northing_m', 'Easting_rounded', 'Northing_rounded', 'Latitude',
       'Longitude', 'FRS', 'IncidentStationGround',
       'FirstPumpArriving_AttendanceTime',
       'FirstPumpArriving_DeployedFromStation',
       'SecondPumpArriving_AttendanceTime',
       'SecondPumpArriving_DeployedFromStation',
       'NumStationsWithPumpsAttending', 'NumPumpsAttending', 'PumpCount',
       'PumpHoursRoundUp', 'Notional Cost (£)', 'NumCalls'],
      dtype='object')
מחקנו מהדאטה עמודות לא רלוונטיות לשאלת המחקר ועמודות בעלות משמעות כפולה
columns_to_drop = [
    "UPRN", "USRN",
    "AddressQualifier",
    "ProperCase",
    "Easting_m", "Northing_m",
    "Easting_rounded", "Northing_rounded",
    "IncGeo_WardCode", "IncGeo_WardName", "IncGeo_WardNameNew",
    
]

df_cleaned = fire_df.drop(columns=columns_to_drop)
df_cleaned.to_csv("fire_cases_cleaned.csv", index=False)
החלטנו לעשות קורלציה לכל הדאטה, על מנת לראות קשרים בין הנתונים ולבחור במה להתעמק ולהתמקד:
correlation_matrix = df_cleaned.corr(numeric_only=True)

plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap="coolwarm", square=True, cbar_kws={"shrink": .8})
plt.title("Correlation Matrix Between Numerical Variables")
plt.tight_layout()
plt.show()
No description has been provided for this image

First path :

היקף הקריאות ביחס למאפיינים תפעוליים וחומרת השריפות

  • האם קיים קשר בין מספר השיחות למוקד הכבאות בלונדון לבין כמות הכבאיות, משך הפעולה ועלות הכיבוי?
  • כיצד משפיעה רמת חומרת השריפה על כמות השיחות הנכנסות?
---

No description has been provided for this image
בהמשך לניתוח הקורלציה שערכנו, בחנו שלושה קשרים עיקריים: בין מספר השיחות לבין מספר הכבאיות שנשלחו לאירוע, העלות המשוערת של הטיפול באירוע, ושעות הפעולה של הכבאיות. מטרת בדיקה זו היא להבין האם אירועים שמעוררים יותר פניות ציבוריות אכן מאופיינים במענה מבצעי משמעותי יותר.
plt.figure(figsize=(14, 6))

color1 = "#f4a261"  
color2 = "#e76f51" 
color3 = "#f7d794" 


plt.subplot(1, 3, 1)
sns.regplot(data=df_cleaned, x='NumCalls', y='PumpCount', scatter_kws={"alpha": 0.4, "color": color1}, line_kws={"color": color1})
plt.title('Number of Calls vs. Number of Pumps Dispatched')
plt.xlabel('Number of Calls')
plt.ylabel('Pump Count')

plt.subplot(1, 3, 2)
sns.regplot(data=df_cleaned, x='NumCalls', y='Notional Cost (£)', scatter_kws={"alpha": 0.4, "color": color2}, line_kws={"color": color2})
plt.title('Number of Calls vs. Estimated Response Cost')
plt.xlabel('Number of Calls')
plt.ylabel('Estimated Cost (£)')

plt.subplot(1, 3, 3)
sns.regplot(data=df_cleaned, x='NumCalls', y='PumpHoursRoundUp', scatter_kws={"alpha": 0.4, "color": color3}, line_kws={"color": color3})
plt.title('Number of Calls vs. Pump Work Hours')
plt.xlabel('Number of Calls')
plt.ylabel('Pump Work Hours')

plt.tight_layout()
plt.show()
No description has been provided for this image
בגרף זיהינו שתי נקודות חריגות: האחת עם כ-20 שיחות, שבה נרשמה הפעלת משאבים וזמן טיפול חריגים בגובהם, והשנייה עם כ-175 שיחות, שבה גם הופעלו משאבים רבים אך בעוצמה מעט פחותה. מטרתנו הייתה לחקור חריגויות אלו כדי להבין מה הוביל להפעלת משאבים יוצאת דופן ביחס למספר השיחות שהתקבלו.
unusual_df = fire_df[(fire_df["NumCalls"] > 170) | (fire_df["PumpHoursRoundUp"] > 1000)]

selected_columns = ["PropertyCategory", "PropertyType", "StopCodeDescription", "AddressQualifier", 
                    "IncGeo_BoroughName", "NumCalls", "Notional Cost (£)", "PumpHoursRoundUp", "PumpCount"]

unusual_df[selected_columns]
PropertyCategory PropertyType StopCodeDescription AddressQualifier IncGeo_BoroughName NumCalls Notional Cost (£) PumpHoursRoundUp PumpCount
37852 Non Residential Warehouse Primary Fire Correct incident location HARINGEY 175.0 280014.0 826.0 179.0
51021 Outdoor Wasteland Primary Fire Open land/water - nearest gazetteer location HAVERING 20.0 407817.0 1203.0 250.0
סביר להניח שההבדל בכמות השיחות נובע לא מהחומרה האובייקטיבית של האירוע, אלא מהחשיפה הציבורית למוקד השריפה – כאשר אירועים באזורים עירוניים נוטים לקבל יותר שיחות גם אם בפועל הם פחות חמורים.

סיננו את הנתונים החריגים שבהם מספר השיחות עולה על 75, והורדנו את הנתונים החריגים בשלושת הקטגוריות הנוספות, וכעת הצגנו מחדש את הגרפים עם חלוקה לקבוצות בטווחי חמש שיחות, כדי להמחיש טוב יותר את ההבדלים בין המשתנים ביחס למספר השיחות.
fire_df.dropna(subset=["NumCalls", "PumpCount", "Notional Cost (£)", "PumpHoursRoundUp"], inplace=True)

df = fire_df[
    (fire_df["NumCalls"] > 0) & (fire_df["NumCalls"] <= 75) &
    (fire_df["Notional Cost (£)"] < 250000) &
    (fire_df["PumpHoursRoundUp"] < 700) &
    (fire_df["PumpCount"] < 150)
].copy() 

df.loc[:, "CallsGroup"] = pd.cut(
    df["NumCalls"],
    bins=[1, 15, 30, 45, 60, 75],
    labels=["1-15 calls", "16-30 calls", "31-45 calls", "46-60 calls", "61-75 calls"]
)

group_means = df.groupby("CallsGroup", observed=False).agg({
    "PumpCount": "mean",
    "Notional Cost (£)": "mean",
    "PumpHoursRoundUp": "mean"
}).reset_index()

fig, axes = plt.subplots(1, 3, figsize=(22, 5))

sns.barplot(data=group_means, x="CallsGroup", y="PumpCount", hue="CallsGroup", palette="Reds", legend=False, ax=axes[0])
axes[0].set_title("Average Number of Pumps Dispatched by Call Group")
axes[0].set_xlabel("Call Group")
axes[0].set_ylabel("Average Pumps Dispatched")
axes[0].grid(True)

sns.barplot(data=group_means, x="CallsGroup", y="Notional Cost (£)", hue="CallsGroup", palette="Oranges", legend=False, ax=axes[1])
axes[1].set_title("Average Incident Cost by Call Group")
axes[1].set_xlabel("Call Group")
axes[1].set_ylabel("Average Incident Cost (£)")
axes[1].grid(True)

sns.barplot(data=group_means, x="CallsGroup", y="PumpHoursRoundUp", hue="CallsGroup", palette="YlOrBr", legend=False, ax=axes[2])
axes[2].set_title("Average Pump Hours by Call Group")
axes[2].set_xlabel("Call Group")
axes[2].set_ylabel("Average Pump Hours")
axes[2].grid(True)

plt.tight_layout()
plt.show()
No description has been provided for this image
בהתבסס על הנתונים, זוהה קשר חיובי מובהק בין מספר השיחות שהתקבלו בנוגע לאירוע לבין עוצמת התגובה המבצעית, המתבטאת במספר המשאבות שהופעלו, עלות הטיפול וזמן הפעולה. ממצא זה מרמז כי מספר השיחות עשוי לשמש כמדד המשקף את חומרת האירוע.

רצינו לבדוק את הקשר בין מספר השיחות שהתקבלו לאירוע לבין רמת החומרה של האירוע, על ידי מיפוי סוגי האירועים לרמות חומרה מספריות והצגת מגמת השינוי בעזרת קו מגמה חלק (lowess), כדי להבין האם אירועים עם יותר שיחות נוטים להיות חמורים יותר.
fire_df['StopCodeDescription'].unique()
array(['Special Service', 'Secondary Fire', 'False alarm - Good intent',
       'AFA', 'Primary Fire', 'False alarm - Malicious', 'Chimney Fire',
       'Late Call', 'Flood call attended - Batch mobilised',
       'Use of Special Operations Room'], dtype=object)
df = df.copy()

severity_map = {
    'AFA': 1,
    'False alarm - Good intent': 1,
    'False alarm - Malicious': 2,
    'Late Call': 2,
    'Use of Special Operations Room': 2,
    'Flood call attended - Batch mobilised': 3,
    'Special Service': 3,
    'Chimney Fire': 3,
    'Secondary Fire': 4,
    'Primary Fire': 5
}

df.loc[:, 'SeverityLevel'] = df['StopCodeDescription'].map(severity_map)
df.loc[:, 'CallsBin'] = pd.cut(df['NumCalls'], bins=[0, 15, 30, 45, 60, 75])

bin_summary = df.groupby('CallsBin', observed=True).agg({
    'SeverityLevel': 'mean',
    'PumpHoursRoundUp': 'mean',
    'PumpCount': 'mean',
    'Notional Cost (£)': 'mean',
    'StopCodeDescription': 'count'
}).reset_index().rename(columns={'StopCodeDescription': 'EventCount'})

bin_summary['CallsBin'] = bin_summary['CallsBin'].astype(str)

fig, axes = plt.subplots(1, 2, figsize=(16, 6), sharex=False)

ax1 = axes[0]
ax1b = ax1.twinx()
color1 = 'darkred'
ax1.plot(bin_summary['CallsBin'], bin_summary['SeverityLevel'], marker='o', color=color1, label='Severity Level', linewidth=2)
ax1.set_ylabel('Average Severity Level', color=color1)
ax1.tick_params(axis='y', labelcolor=color1)
ax1.set_ylim(1, 5.2)

for i, (sev, count) in enumerate(zip(bin_summary['SeverityLevel'], bin_summary['EventCount'])):
    ax1.text(i, sev + 0.15, f'Count: {count}', ha='center', color=color1, fontsize=9)

color2 = 'darkblue'
color3 = 'green'
ax1b.plot(bin_summary['CallsBin'], bin_summary['PumpHoursRoundUp'], marker='s', color=color2, label='Pump Hours', linewidth=2)
ax1b.plot(bin_summary['CallsBin'], bin_summary['PumpCount'], marker='^', color=color3, label='Pump Count', linewidth=2)
ax1b.set_ylabel('Avg Pump Hours / Pump Count', color='black')
ax1b.tick_params(axis='y', labelcolor='black')

ax1.set_title('Severity vs. Pump Hours & Count')
ax1.set_xlabel('Number of Calls (Binned)')
ax1.grid(True, linestyle='--', alpha=0.5)
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax1b.get_legend_handles_labels()
ax1b.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

ax2 = axes[1]
ax2b = ax2.twinx()
ax2.plot(bin_summary['CallsBin'], bin_summary['SeverityLevel'], marker='o', color=color1, label='Severity Level', linewidth=2)
ax2.set_ylabel('Average Severity Level', color=color1)
ax2.tick_params(axis='y', labelcolor=color1)
ax2.set_ylim(1, 5.2)

color4 = 'darkorange'
ax2b.plot(bin_summary['CallsBin'], bin_summary['Notional Cost (£)'], marker='D', color=color4, label='Notional Cost (£)', linewidth=2)
ax2b.set_ylabel('Average Notional Cost (£)', color=color4)
ax2b.tick_params(axis='y', labelcolor=color4)

for i, (sev, count) in enumerate(zip(bin_summary['SeverityLevel'], bin_summary['EventCount'])):
    ax2.text(i, sev + 0.15, f'Count: {count}', ha='center', color=color1, fontsize=9)

ax2.set_title('Severity vs. Notional Cost')
ax2.set_xlabel('Number of Calls (Binned)')
ax2.grid(True, linestyle='--', alpha=0.5)
lines1, labels1 = ax2.get_legend_handles_labels()
lines2, labels2 = ax2b.get_legend_handles_labels()
ax2b.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

fig.suptitle('Fire Event Severity and Resources by Number of Calls', fontsize=16)
plt.tight_layout()
plt.show()
No description has been provided for this image
ככל שמספר השיחות לאירוע עולה, מושקעים יותר משאבים והעלות המשוערת גבוהה יותר. עם זאת, רמת החומרה לא תמיד עולה בהתאם – ייתכן שהירידה בחומרה בקבוצות עם מספר שיחות גבוה נובעת ממיעוט מקרים בקבוצות אלה, ולכן המגמה אינה בהכרח מייצגת.

מסקנות עיקריות:

    מהשוואת הגרפים עולה כי ככל שמספר השיחות לאירוע גבוה יותר, ניכרת עלייה בשימוש במשאבים – הן במספר המשאבות והן בשעות העבודה ובעלות המשוערת. עם זאת, רמת החומרה הממוצעת אינה ממשיכה לעלות באותו קצב ואף יורדת בקבוצת השיחות הגבוהה ביותר. ייתכן שהסבר לכך טמון בכך שבקבוצה זו יש מעט מאוד מקרים, ולכן הממוצעים בה פחות מייצגים. בנוסף, ייתכן שמקרים אלו התרחשו במיקומים ציבוריים – מה שגרם לריבוי שיחות, אך לא בהכרח מעיד על חומרת שריפה גבוהה, כפי שניתן היה לראות גם בשני האירועים החריגים שניתחנו.

second path :

התפלגות גאוגרפית של אירועי שריפה בלונדון והשפעתם לפי אזור

  • מהי ההשפעה של מאפיינים גאוגרפיים ודמוגרפיים, כגון מיקום השכונה וצפיפות האוכלוסייה, על היקף אירועי השריפות וסוגי הרכוש שנפגעו מהם בעיר לונדון?

No description has been provided for this image
ניסינו לבדוק ולדרג את שכונות לונדון לפי כמות אירועי השריפות, מהשכונה עם מספר השריפות הגבוה ביותר ועד לזו עם המספר הנמוך ביותר.
borough_counts = df_cleaned['IncGeo_BoroughName'].value_counts().reset_index()
borough_counts.columns = ['Boroughs', 'Count']

plt.figure(figsize=(12, 8))

palette = sns.light_palette("#c75c4c", n_colors=len(borough_counts), reverse=True)

sns.barplot(data=borough_counts, x='Count', y='Boroughs', hue='Boroughs', palette=palette, dodge=False, legend=False)

plt.title('Number of Incidents by Borough (IncGeo_BoroughName)', fontsize=14)
plt.xlabel('Number of Incidents', fontsize=12)
plt.ylabel('Boroughs', fontsize=12)
plt.tight_layout()
plt.show()
No description has been provided for this image
הצגנו את הפיזור הגאוגרפי של אירועי השריפות ברחבי לונדון באמצעות מפת חום.
borough_counts['Boroughs'] = borough_counts['Boroughs'].str.strip().str.upper()

url = "https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/london_boroughs.geojson"
response = requests.get(url)
geojson = response.json()

for feature in geojson['features']:
    feature['properties']['name'] = feature['properties']['name'].strip().upper()

fig = px.choropleth_mapbox(
    borough_counts,
    geojson=geojson,
    locations='Boroughs',
    featureidkey='properties.name',
    color='Count',
    color_continuous_scale="YlOrRd",
    mapbox_style="carto-positron",
    zoom=9,
    center={"lat": 51.5074, "lon": -0.1278},
    opacity=0.6,
    labels={'Count': 'מספר אירועים'}
)

fig.update_layout(margin={"r":0,"t":30,"l":0,"b":0}, title_text='מספר אירועים לפי שכונות בלונדון')
fig.show()

מהניתוח של שני הגרפים הראשונים עולה כי מספר השריפות בלונדון מרוכז בעיקר באזור המרכזי של העיר. הערים המרכזיות, כגון Westminster ו-Camden, מציגות את שיעור השריפות הגבוה ביותר, בעוד שהאזורים המרוחקים יותר מהמרכז מאופיינים בשיעור שריפות נמוך יותר מסקנה זו מצביעה על כך שהסיכון לשריפות גבוה יותר באזורים בעלי צפיפות עירונית גבוהה.

בדקנו האם יש קשר בין גודל האוכלוסייה לבין מספר השריפות בכל שכונה. לשם כך, שילבנו קובץ חיצוני שמכיל נתונים על כמות האוכלוסייה בכל אחת משכונות לונדון, והשווינו אותם למספר אירועי השריפות באותן שכונות.
url_population = "https://raw.githubusercontent.com/YeheliRot/Fiers-cases-in-London/refs/heads/main/Population.csv"
pop_df = pd.read_csv(url_population)

fire_counts = df_cleaned['IncGeo_BoroughName'].value_counts().reset_index()
fire_counts.columns = ['Boroughs', 'FireCount']

fire_counts['Boroughs'] = fire_counts['Boroughs'].str.upper()
pop_df['Boroughs'] = pop_df['Boroughs'].str.upper()

merged_df = pd.merge(pop_df, fire_counts, on='Boroughs', how='left')
merged_df['FireCount'] = merged_df['FireCount'].fillna(0)

corr = merged_df['Population'].corr(merged_df['FireCount'])
print(f"Pearson correlation between Population and FireCount: {corr:.2f}")

sns.lmplot(data=merged_df, x='Population', y='FireCount', height=6, aspect=1.2)
plt.title(f"Correlation between Population and FireCount: {corr:.2f}")
plt.show()
Pearson correlation between Population and FireCount: 0.37
No description has been provided for this image
מהגרף עולה כי קיימת קורלציה חיובית מתונה (0.37) בין כמות האוכלוסייה בכל שכונה לבין מספר השריפות שהתרחשו בה. ממצא זה מצביע על כך שככל שכמות האוכלוסייה בשכונה גבוהה יותר, כך יש נטייה לעלייה במספר אירועי השריפות. עם זאת, מאחר והקורלציה אינה גבוהה, ניתן להסיק כי אמנם צפיפות אוכלוסייה משפיעה במידה מסוימת על שכיחות השריפות, אך קיימים גם גורמים נוספים התורמים לתופעה זו.
table = merged_df[['Boroughs', 'Population', 'FireCount']].sort_values(by='FireCount', ascending=False)
display(table)
Boroughs Population FireCount
32 WESTMINSTER 211365 24024
5 CAMDEN 210968 14877
27 SOUTHWARK 306762 14602
29 TOWER HAMLETS 312715 14023
21 LAMBETH 317691 13874
7 CROYDON 390691 13531
11 HACKNEY 260082 13238
22 LEWISHAM 300013 11816
1 BARNET 388955 11414
19 KENSINGTON AND CHELSEA 144266 11147
8 EALING 366762 11136
24 NEWHAM 351256 10858
18 ISLINGTON 217050 10737
3 BRENT 339248 10406
16 HILLINGDON 305107 10204
31 WANDSWORTH 328429 10202
9 ENFIELD 329876 10117
13 HARINGEY 264317 10072
10 GREENWICH 289537 9886
4 BROMLEY 329899 9685
12 HAMMERSMITH AND FULHAM 183310 9503
30 WALTHAM FOREST 278147 9346
17 HOUNSLOW 288164 8865
25 REDBRIDGE 31010 7737
15 HAVERING 262086 7158
2 BEXLEY 246637 6975
0 BARKING AND DAGENHAM 218714 6318
14 HARROW 261159 5679
23 MERTON 215426 5574
28 SUTTON 209617 5541
26 RICHMOND UPON THAMES 195315 5405
20 KINGSTON UPON THAMES 167992 4318
6 CITY OF LONDON 8689 3302
ניתחנו את סוגי הרכוש שנפגעו מהשריפות, במטרה לזהות אילו סוגי נכסים (מגורים, שטחים פתוחים, רכבים וכדומה) היו החשופים ביותר לפגיעה, ולהבין האם מרבית השריפות התרחשו באזורים עירוניים או בסביבות טבעיות.
plt.figure(figsize=(12, 6))
sns.countplot(
    data=df_cleaned,
    x='PropertyCategory',
    hue='PropertyCategory',
    order=df_cleaned['PropertyCategory'].value_counts().index,
    palette='rocket_r',
    legend=False
)
plt.title('Number of Fires by Property Category', fontsize=16)
plt.xlabel('Property Category', fontsize=14)
plt.ylabel('Number of Fires', fontsize=14)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
No description has been provided for this image

מסקנה:

מניתוח הנתונים עולה כי אירועי השריפות בלונדון אינם מתפלגים באופן אקראי, אלא מתרכזים בעיקר באזורים המרכזיים של העיר, המאופיינים בצפיפות אוכלוסייה גבוהה. הקורלציה שנמצאה בין כמות האוכלוסייה למספר השריפות מצביעה על קשר מסוים, אך לא בלעדי, בין צפיפות לבין סיכון לשריפות. בנוסף, ניתוח סוגי הרכוש שנפגעו מצביע על כך שהנזקים מתרכזים בעיקר בנכסים עירוניים, מה שמחזק את המסקנה כי מרבית השריפות מתרחשות באזורים מיושבים.

Third path :

"ההשפעה הכלכלית של אירועי שריפה ברחבי לונדון"

"

  • כיצד משתנים העלויות של מקרי כיבוי אש בלונדון בהתאם למיקומם הגיאוגרפי ולאופי הקריאה, ומהם הגורמים המרכזיים המשפיעים על רמת העלות?

No description has been provided for this image

רצינו לבדוק באילו שכונות בלונדון העלות הכוללת של הכיבוי היא הגבוהה ביותר, והאם קיימת התאמה בין מספר מקרי השריפה לבין העלות הכוללת בשכונות השונות.
incident_counts = df['IncGeo_BoroughName'].value_counts().reset_index()
incident_counts.columns = ['Borough', 'Count']

costs_by_borough = df_cleaned.groupby('IncGeo_BoroughName')['Notional Cost (£)'].sum().reset_index()
costs_by_borough.columns = ['Borough', 'TotalCost']

merged = pd.merge(incident_counts, costs_by_borough, on='Borough')
merged = merged.sort_values(by='Count', ascending=False)

palette = sns.light_palette("#c75c4c", n_colors=len(merged), reverse=True)
borough_palette = dict(zip(merged['Borough'], palette))

fig, axes = plt.subplots(1, 2, figsize=(18, 10), sharey=True)

sns.barplot(
    ax=axes[0], data=merged, x='Count', y='Borough',
    hue='Borough', palette=borough_palette, legend=False
)
axes[0].set_title('Number of Incidents by Borough', fontsize=14)
axes[0].set_xlabel('Number of Incidents', fontsize=12)
axes[0].set_ylabel('Borough', fontsize=12)

merged_sorted_by_cost = merged.sort_values(by='TotalCost', ascending=False)
sns.barplot(
    ax=axes[1], data=merged_sorted_by_cost, x='TotalCost', y='Borough',
    hue='Borough',
    palette=[borough_palette[b] for b in merged_sorted_by_cost['Borough']],
    legend=False
)
axes[1].set_title('Total Notional Cost (£) by Borough', fontsize=14)
axes[1].set_xlabel('Total Notional Cost (£)', fontsize=12)
axes[1].set_ylabel('')

plt.tight_layout()
plt.show()
No description has been provided for this image

מהגרף ניתן לראות שלרוב יש התאמה בין מספר מקרי השריפה לעלות הכוללת, אך ישנם שכונות שבהם העלות גבוהה למרות מספר נמוך יותר של אירועים.


הסרנו את שנת 2022 מהניתוח כי הנתונים עבורה חלקיים (רק עד פברואר), והשוואה לשנים שלמות הייתה יוצרת הטיה בתוצאות.
filtered_df = fire_df[fire_df["CalYear"] != 2022]
הגרף מציג את העלות הכוללת של קריאות לשירותי הכבאות בשנים 2019–2021. הניתוח בוצע במטרה לבחון את השינויים בעלות הכוללת לאורך השנים.

בגרף זה נבחנה העלות הכוללת של קריאות לשירותי הכבאות בהתאם לסוג האירוע והעלות השנתית הכוללת. מטרת הניתוח הייתה לזהות אילו סוגי אירועים גוררים את העלות הגבוהה ביותר עבור שירותי הכבאות והאם יש הבדלים בין השנים.
 df_cleaned = filtered_df[["CalYear", "IncidentGroup", "Notional Cost (£)"]].dropna()

 incident_costs = df_cleaned.groupby(["CalYear", "IncidentGroup"])["Notional Cost (£)"].sum().unstack(fill_value=0)
 yearly_costs = df_cleaned.groupby("CalYear")["Notional Cost (£)"].sum()

 years = incident_costs.index.astype(str)
 categories = incident_costs.columns
 x = np.arange(len(years))
 width = 0.25

 colors = sns.light_palette("#c75c4c", n_colors=len(categories), reverse=True)

 plt.figure(figsize=(10, 6))

 for i, cat in enumerate(categories):
     plt.bar(x + i*width, incident_costs[cat], width=width, label=cat, color=colors[i], edgecolor="black")

 for i, year in enumerate(years):
     total_cost = yearly_costs[int(year)]
     center_x = x[i] + width  
     plt.text(center_x, incident_costs.iloc[i].max() + 50000, f{total_cost:,.0f}", ha='center', fontsize=9, fontweight='bold', color='#5a2e2e')

 plt.xticks(x + width, years)
 plt.title("Incident Types by Year with Total Cost (excluding 2022)", fontsize=14)
 plt.xlabel("Year")
 plt.ylabel("Total Cost (£)")
 plt.legend(title="Incident Type")
 plt.grid(axis='y', linestyle='--', alpha=0.4)
 plt.tight_layout()
 plt.show()
No description has been provided for this image
בגרף זה בדקנו את היקף הקריאות מכל סוג, בעקבות הממצא שקריאות שווא הן היקרות ביותר. המטרה הייתה להבין האם העלות הגבוהה נובעת מכמות גדולה של קריאות מסוג זה.
def classify_call(row):
    if row['IncidentGroup'] == 'False Alarm':  
        return 'False Alarms'
    elif row['IncidentGroup'] == 'Fire':      
        return 'Real Fire Calls'
    else:
        return 'Special Cases'

df.loc[:, 'CallType'] = df.apply(classify_call, axis=1)

call_counts = df['CallType'].value_counts()

plt.figure(figsize=(7, 7))

palette = sns.light_palette("#c75c4c", n_colors=3, reverse=True)

plt.pie(
    call_counts,
    labels=call_counts.index,
    autopct='%1.1f%%',
    startangle=140,
    colors=palette
)

plt.title('Distribution of Fire Department Calls by Type')
plt.axis('equal')
plt.show()
No description has been provided for this image
נבדוק מה סוג האירוע בפועל שגורם למרבית קריאות השווא
false_alarms_df = df[df['CallType'] == 'False Alarms']
false_alarm_types = false_alarms_df['StopCodeDescription'].value_counts()
false_alarm_types
StopCodeDescription
AFA                          127968
False alarm - Good intent     34769
False alarm - Malicious        3235
Name: count, dtype: int64
סוג האירוע עם הרוב המוחלט של קריאות שווא הוא AFA-אזעקה אוטומטית
calls_by_type_borough = df.groupby(['IncGeo_BoroughName', 'CallType']).size().unstack(fill_value=0)
calls_by_type_borough['Total Calls'] = calls_by_type_borough.sum(axis=1)
calls_by_type_borough['False Alarm Ratio'] = calls_by_type_borough['False Alarms'] / calls_by_type_borough['Total Calls']
calls_by_type_borough['Real Call Ratio'] = 1 - calls_by_type_borough['False Alarm Ratio']

cost_per_borough = df.groupby('IncGeo_BoroughName')['Notional Cost (£)'].sum()

data = pd.DataFrame({
    'Cost': cost_per_borough,
    'False Alarm Ratio': calls_by_type_borough['False Alarm Ratio'],
    'Real Call Ratio': calls_by_type_borough['Real Call Ratio'],
}).fillna(0)
data = data.sort_values(by='Cost', ascending=False)
data['Cost False Alarms'] = data['Cost'] * data['False Alarm Ratio']
data['Cost Real Calls'] = data['Cost'] * data['Real Call Ratio']

fig, ax = plt.subplots(figsize=(14, 8))

bars_real = ax.bar(data.index, data['Cost Real Calls'], color='tomato', label='Real Calls (Fire + Special)')
bars_false = ax.bar(data.index, data['Cost False Alarms'], bottom=data['Cost Real Calls'], color='orange', label='False Alarms')

ax.set_ylabel('Cost (£)', fontsize=12)
ax.set_xlabel('Borough', fontsize=12)
ax.set_title('Total Cost per Borough divided by Call Type', fontsize=16)

ax.set_xticks(range(len(data.index)))
ax.set_xticklabels(data.index, rotation=45, ha='right')

ax.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image
הגרף מציג את הפיצול בין עלויות של התרעות שווא לאירועים אמיתיים בכל שכונה, ומדגיש כי באזורים מסוימים התרעות השווא מהוות את רוב העלות הכוללת.

מסקנה:

בהתבסס על הנתונים המוצגים בגרפים, עולה כי מרבית האירועים במחוזות לונדון הם התרעות שווא, אשר מהוות את הרכיב המרכזי בעלויות התפעוליות של המערכת לאורך השנים. נתוני סיבת האירוע מצביעים על כך ש-127,968 מתוך מקרי התרעות השווא נגרמו כתוצאה מאזעקות אוטומטיות (AFA), מה שמעיד על השפעה משמעותית של סוג זה על כלל המשאבים והעלויות. מסקנה זו מקבלת חיזוק משמעותי גם מהגרף המציג את העלות הכוללת לפי שכונה, המחולקת לפי סוג קריאה: מהגרף עולה כי ברוב השכונות חלק ניכר מהעלויות נובע מהתרעות שווא – לעיתים אף יותר מהעלויות של אירועים אמיתיים (שריפות ושירותים מיוחדים). נתון זה מדגיש את תרומתה המרכזית של תופעת התרעות השווא, ובפרט אזעקות אוטומטיות, לנטל הכלכלי על שירותי החירום העירוניים.

Summery:

הניתוח מצביע על כך שככל שמספר השיחות לאירוע גבוה יותר, כך גם השימוש במשאבים עולה, אך כאשר כמות השיחות גבוהה רמת החומרה אינה בהכרח מתגברת בהתאם, ככל הנראה מאחר ומדובר בקבוצות קטנות. שריפות מרוכזות בעיקר באזורים עירוניים צפופי אוכלוסין, דבר שמצביע על קשר מסוים בין צפיפות לבין הסיכון לשריפות. בנוסף, התרעות שווא – ובעיקר אזעקות אוטומטיות (AFA) – מהוות מרכיב משמעותי בעלות הכוללת של האירועים, ומשפיעות באופן מהותי על העומס על מערך החירום.

No description has been provided for this image